using Fusion.Frameworks.Assets;
using Fusion.Async.Timers;
using Fusion.Utilities.Singleton;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Fusion.Frameworks.Audio
{
    public class AudioPlayer
    {
        private AudioSource audioSource;
        public AudioSource AudioSource { get => audioSource; }

        private GameObject bindedObject = null;
        public GameObject BindedObject { get => bindedObject; }

        private string audioName = null;
        public string AudioName { get => audioName; }

        private string tag = "Default";
        public string Tag { get => tag; }

        private float volume = 1.0f;

        private float fadeIn = 0.0f;
        private float fadeInStep = 0.1f;
        private float fadeOut = 0.0f;
        private float fadeOutStep = 0.1f;

        private Timer volumeTimer = null;

        private bool persistent = false;
        public bool Persistent { get => persistent; }

        private GameObject audioObject = null;
        public bool IsValid()
        {
            return audioSource != null;
        }

        public AudioPlayer() : this(AudioManager.Instance.gameObject, false)
        {
        }

        public AudioPlayer(GameObject bindObject) : this(bindObject, false)
        {
        }

        public AudioPlayer(bool persistent) : this(AudioManager.Instance.gameObject, persistent)
        {
        }

        public AudioPlayer(GameObject bindObject, bool persistent)
        {
            this.persistent = persistent;
            AudioManager.Instance.RegisterAudioPlayer(this);
            audioObject = new GameObject("AudioPlayer");
            audioSource = audioObject.AddComponent<AudioSource>();
            BindObject(bindObject);
            Init();
        }

        public void Init()
        {
            tag = "Default";
            volume = 1.0f;
            fadeIn = 0.0f;
            fadeInStep = 0.1f;
            fadeOut = 0.0f;
            fadeOutStep = 0.1f;
        }

        public AudioPlayer BindObject(GameObject bindObject)
        {
            if (!IsValid())
                return this;

            bindedObject = bindObject;
            audioSource.transform.SetParent(bindObject.transform, false);
            audioSource.transform.localPosition = Vector3.zero;
            return this;
        }

        public AudioPlayer MaxDistanceSetter(float value)
        {
            if (!IsValid())
                return this;
            audioSource.maxDistance = value;
            return this;
        }

        public AudioPlayer MinDistanceSetter(float value)
        {
            if (!IsValid())
                return this;
            audioSource.minDistance = value;
            return this;
        }

        public AudioPlayer FadeInSetter(float duration)
        {
            if (!IsValid())
                return this;
            fadeIn = duration;
            return this;
        }

        private void HandleFadeIn(Action finishCallback = null)
        {
            if (fadeIn <= 0.0f)
            {
                if (finishCallback != null)
                {
                    finishCallback();
                }
                return;
            }
            float speed = volume / fadeIn * fadeInStep;
            FadeVolumeSetter(0.0f, volume, speed, finishCallback);
        }

        public AudioPlayer FadeOutSetter(float duration)
        {
            if (!IsValid())
                return this;
            fadeOut = duration;
            return this;
        }

        public void FadeVolumeSetter(float initVolume, float volume, float speed, Action finishCallback = null)
        {
            if (!IsValid())
                return;
            SetVolume(initVolume);
            float currentVolume = initVolume;
            float diff = volume - currentVolume;
            float distance = Mathf.Abs(diff);
            volumeTimer?.Stop();
            if (distance > 0)
            {
                speed = speed * (diff / distance);
                volumeTimer = new Timer().FirstImmediatelySetter(true).FrequencySetter(fadeInStep).PersistentSetter(persistent).TagSetter("AudioManager_Volume").Invoke(delegate ()
                {
                    currentVolume += speed;
                    diff = volume - currentVolume;
                    if (Mathf.Abs(diff) < 0.01f)
                    {
                        currentVolume = volume;
                        SetVolume(currentVolume);
                        volumeTimer?.Stop();
                        if (finishCallback != null)
                        {
                            finishCallback();
                        }
                    }
                    else
                    {
                        SetVolume(currentVolume);
                    }
                });
            }
            else
            {
                if (finishCallback != null)
                {
                    finishCallback();
                }
            }
            
        }

        private void HandleFadeOut(Action finishCallback = null)
        {
            if (fadeOut <= 0.0f || audioSource.clip == null)
            {
                if (finishCallback != null)
                {
                    finishCallback();
                }
                return;
            }
            float speed = volume / fadeOut * fadeOutStep;
            FadeVolumeSetter(volume, 0.0f, speed, finishCallback);
        }

        public AudioPlayer LoopSetter(bool value)
        {
            if (!IsValid())
                return this;
            audioSource.loop = value;
            return this;
        }

        public AudioPlayer TagSetter(string tag)
        {
            if (!IsValid())
                return this;
            this.tag = tag;
            VolumeSetter(volume);
            return this;
        }

        public AudioPlayer VolumeSetter(float value)
        {
            if (value < 0.0f || value > 1.0f)
            {
                throw new ArgumentOutOfRangeException($"volume range must between [0.0, 1.0], current is {value}");
            }

            if (!IsValid())
                return null;
            volume = value;
            SetVolume(volume);
            return this;
        }

        private void SetVolume(float volume)
        {
            if (audioSource == null)
                return;
            AudioManager.AudioTagConfig audioTagConfig = AudioManager.Instance.GetOrAddAudioTagConfig(tag);
            audioSource.volume = volume * audioTagConfig.Volume;
        }

        public void Pause()
        {
            if (!IsValid())
                return;
            HandleFadeOut(delegate ()
            { 
                audioSource.Pause();
            });
        }

        public void Stop()
        {
            if (!IsValid())
                return;
            asyncPlayable = false;
            HandleFadeOut(delegate ()
            {
                audioSource.Stop();
            });
        }

        public void Play(string audio)
        {
            if (!IsValid())
                return;
            string lastAudioName = audioName;
            audioName = audio;
            asyncPlayTimer?.Stop();
            HandleFadeOut(delegate ()
            {
                if (audioName != lastAudioName)
                {
                    if (persistent)
                    {
                        AssetsUtility.ReleaseImmediate(audio, true);
                        AudioClip audioClip = AssetsManager.Instance.Load<AudioClip>(audio, true);
                        audioSource.clip = audioClip;
                    }
                    else
                    {
                        AssetsUtility.SetAudioSourceClip(audioSource, audio);
                    }
                }
                    
                HandleFadeIn();
                audioSource.Play();
            });
        }

        private bool asyncFadeOutOver = false;
        private bool asyncAssetLoadOver = false;
        private bool asyncPlayable = true;
        private Timer asyncPlayTimer = null;

        public bool IsPlaying()
        {
            return asyncPlayTimer != null || audioSource.isPlaying;
        }


        public void PlayAsync(string audio)
        {
            if (!IsValid())
                return;
            string lastAudioName = audioName;
            audioName = audio;
            asyncFadeOutOver = false;
            asyncAssetLoadOver = false;
            asyncPlayable = true;
            asyncPlayTimer?.Stop();
            HandleFadeOut(delegate ()
            {
                asyncFadeOutOver = true;
            });

            if (audioName != lastAudioName)
            {
                if (persistent)
                {

                    AssetsManager.Instance.LoadAsync(audio, delegate (AudioClip audioClip)
                    {
                        audioSource.clip = audioClip;
                        asyncAssetLoadOver = true;
                        AssetsUtility.ReleaseImmediate(lastAudioName, true);
                    }, true);
                }
                else
                {
                    AssetsUtility.SetAudioSourceClipAsync(audioSource, audio, delegate ()
                    {
                        asyncAssetLoadOver = true;
                    });
                }
            }
            else
            {
                asyncAssetLoadOver = true;
            }


            asyncPlayTimer = new Timer().TypeSetter(Timer.InvokeType.Frame).TagSetter("AudioManager_AsyncPlay").PersistentSetter(persistent).Invoke(delegate ()
            {
                if (asyncFadeOutOver && asyncAssetLoadOver)
                {
                    if (audioSource != null && asyncPlayable)
                    {
                        HandleFadeIn();
                        audioSource.Play();
                    }
                    asyncPlayTimer?.Stop();
                    asyncPlayTimer = null;
                }
            });
        }

        public void Release()
        {
            if (!IsValid())
                return;
            audioSource.Stop();
            asyncPlayTimer?.Stop();
            volumeTimer?.Stop();
            asyncPlayTimer = null;
            volumeTimer = null;
            if (persistent)
            {
                AssetsUtility.ReleaseImmediate(audioName, true);
            }
            AssetsUtility.Release(audioObject);
            audioSource = null;
        }
    }

    [DisallowMultipleComponent]
    public class AudioManager : SingletonBehaviour<AudioManager>
    {
        public class AudioTagConfig
        {
            private float volume = 1.0f;

            public float Volume { 
                get => volume; 
                set 
                {
                    if (value < 0.0f || value > 1.0f)
                    {
                        throw new ArgumentOutOfRangeException($"volume range must between [0.0, 1.0], current is {value}");
                    }
                    volume = value; 
                } 
            }
        }

        private Dictionary<string, AudioTagConfig> AudioTagConfigs = new Dictionary<string, AudioTagConfig>();

        private List<AudioPlayer> audioPlayers = new List<AudioPlayer>();

        public void RegisterAudioPlayer(AudioPlayer audioPlayer)
        {
            audioPlayers.Add(audioPlayer); 
        }

        private void HandleAudioPlayers(Action<AudioPlayer> callback)
        {
            for (int i = audioPlayers.Count - 1; i >= 0; i--)
            {
                AudioPlayer audioPlayer = audioPlayers[i];
                if (audioPlayer.AudioSource != null)
                {
                    callback(audioPlayer);
                }
                else
                {
                    audioPlayers.Remove(audioPlayer);
                }
            }
        }

        public void SetTagVolume(string tag, float volume)
        {
            AudioTagConfig audioTagConfig = GetOrAddAudioTagConfig(tag);
            audioTagConfig.Volume = volume;
            HandleAudioPlayers(delegate (AudioPlayer audioPlayer)
            {
                if (audioPlayer.Tag == tag)
                {
                    audioPlayer.TagSetter(tag);
                }
            });
        }

        public void Clear()
        {
            HandleAudioPlayers(delegate (AudioPlayer audioPlayer)
            {
                if (!audioPlayer.Persistent)
                {
                    audioPlayer.Release();
                    audioPlayers.Remove(audioPlayer);
                }
            });
        }

        public AudioTagConfig GetOrAddAudioTagConfig(string clipTag)
        {
            AudioTagConfig audioTagConfig;
            if (!AudioTagConfigs.TryGetValue(clipTag, out audioTagConfig))
            { 
                audioTagConfig = new AudioTagConfig();
                AudioTagConfigs.Add(clipTag, audioTagConfig);
            }
            return audioTagConfig;
        }
    }
}
